/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007-2008 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.projects.csv.business.manager.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.XmlMappingManager;
import com.genia.toolbox.projects.csv.bean.Binder;
import com.genia.toolbox.projects.csv.bean.BinderVisitor;
import com.genia.toolbox.projects.csv.bean.EntityBinder;
import com.genia.toolbox.projects.csv.bean.EntityDescriptor;
import com.genia.toolbox.projects.csv.bean.FieldDescriptor;
import com.genia.toolbox.projects.csv.bean.FolderDescriptor;
import com.genia.toolbox.projects.csv.bean.MappingDescriptor;
import com.genia.toolbox.projects.csv.bean.MutableFieldDescriptor;
import com.genia.toolbox.projects.csv.bean.impl.BinderVisitorAdapter;
import com.genia.toolbox.projects.csv.business.exception.MappingException;
import com.genia.toolbox.projects.csv.business.manager.MappingDescriptorManager;
import com.genia.toolbox.projects.csv.enumeration.FieldType;
import com.genia.toolbox.projects.toolbox_basics_project.bean.model.I18nDynamicValue;

/**
 * implementation of {@link MappingDescriptorManager}.
 */
public class MappingDescriptorManagerImpl
    implements MappingDescriptorManager
{

  /**
   * visitor that allows to compute the priority of an {@link EntityDescriptor}.
   */
  private class EntityPriorityVisitor
      extends BinderVisitorAdapter
  {
    /**
     * the current priority of the entity.
     */
    private int computedPriority = 0;

    /**
     * the list of priorities.
     */
    private final Map<EntityDescriptor, Integer> priorities;



    /**
     * constructor.
     * 
     * @param entityDescriptor
     *          the {@link EntityDescriptor} to compute the priority for
     * @param priorities
     *          the list of priorities
     */
    public EntityPriorityVisitor(EntityDescriptor entityDescriptor, Map<EntityDescriptor, Integer> priorities)
    {
      this.priorities = priorities;
      priorities.put(entityDescriptor, -1);
    }



    /**
     * getter for the computedPriority property.
     * 
     * @return the computedPriority
     */
    public int getComputedPriority()
    {
      return computedPriority;
    }



    /**
     * visit method for {@link EntityBinder}.
     * 
     * @param binder
     *          the binder to visit
     * @throws BundledException
     *           if an error occured
     * @see com.genia.toolbox.projects.csv.bean.impl.BinderVisitorAdapter#visitEntityBinder(com.genia.toolbox.projects.csv.bean.EntityBinder)
     */
    @Override
    public void visitEntityBinder(EntityBinder binder)
        throws BundledException
    {
      int newPriority = initAndGetPriority(binder.getBindedEntity(), priorities) + 1;
      if (computedPriority < newPriority) {
        computedPriority = newPriority;
      }
    }

  }

  /**
   * error key for circular dependency of entities.
   */
  public static final String KEY_ENTITY_CIRCULAR_DEPENDENCY = MappingDescriptorManagerImpl.class.getName() + ".error.entityCircularDependency";

  /**
   * error key for an entity with no class defined.
   */
  public static final String KEY_ENTITY_WITH_NO_CLASS = MappingDescriptorManagerImpl.class.getName() + ".error.entityWithNoClass";

  /**
   * error key for an entity with no name defined.
   */
  public static final String KEY_ENTITY_WITH_NO_NAME = MappingDescriptorManagerImpl.class.getName() + ".error.entityWithNoName";

  /**
   * error key for circular dependency of folder.
   */
  public static final String KEY_FOLDER_CIRCULAR_DEPENDENCY = MappingDescriptorManagerImpl.class.getName() + ".error.folderCircularDependency";

  /**
   * language not supported error message.
   */
  public static final String KEY_LANGUAGE_NOT_SUPPORTED = MappingDescriptorManagerImpl.class.getName() + ".error.languageNotSupported";

  /**
   * a field type is incompatible with the given class.
   */
  public static final String KEY_FIELD_TYPE_IS_INCOMPATIBLE_WITH_CLASS = MappingDescriptorManagerImpl.class.getName() + ".error.fieldTypeIsIncompatibleWithClass";

  /**
   * the class of a field indexed by its field.
   */
  private static final Map<FieldType, Class<?>> CLASS_BY_TYPE = new HashMap<FieldType, Class<?>>();

  /**
   * a {@link BinderVisitor} that initializer the binders.
   */
  private static final BinderVisitor INITIALIZER_BINDER_VISITOR = new InitializerBinderVisitor();

  /**
   * the type of a field indexed by its class.
   */
  private static final Map<Class<?>, FieldType> TYPE_BY_CLASS = new HashMap<Class<?>, FieldType>();

  static {
    TYPE_BY_CLASS.put(Short.class, FieldType.INTEGER);
    TYPE_BY_CLASS.put(Integer.class, FieldType.INTEGER);
    TYPE_BY_CLASS.put(Long.class, FieldType.INTEGER);
    TYPE_BY_CLASS.put(Float.class, FieldType.DECIMAL);
    TYPE_BY_CLASS.put(Double.class, FieldType.DECIMAL);
    TYPE_BY_CLASS.put(Boolean.class, FieldType.BOOLEAN);

    CLASS_BY_TYPE.put(FieldType.DATE, Date.class);
    CLASS_BY_TYPE.put(FieldType.TIME, Date.class);
    CLASS_BY_TYPE.put(FieldType.DATETIME, Date.class);
    CLASS_BY_TYPE.put(FieldType.DIRECTORY, String.class);
    CLASS_BY_TYPE.put(FieldType.FILE, String.class);
    CLASS_BY_TYPE.put(FieldType.I18N, I18nDynamicValue.class);
    CLASS_BY_TYPE.put(FieldType.I18N_BIND, I18nDynamicValue.class);
    CLASS_BY_TYPE.put(FieldType.BOOLEAN, Boolean.class);
  }

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * the {@link XmlMappingManager} to retrieve the {@link MappingDescriptor}.
   */
  private XmlMappingManager xmlMappingManager;



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * retrieve the {@link MappingDescriptor} describe in the given xml file.
   * 
   * @param file
   *          the xml file.
   * @return the {@link MappingDescriptor} describe in the given xml file
   * @throws BundledException
   *           if an error occured
   * @see com.genia.toolbox.projects.csv.business.manager.MappingDescriptorManager#getMappingDescriptor(java.io.File)
   */
  public MappingDescriptor getMappingDescriptor(File file)
      throws BundledException
  {
    try {
      return getMappingDescriptor(new FileInputStream(file));
    }
    catch (FileNotFoundException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * retrieve the {@link MappingDescriptor} describe in the given xml input.
   * 
   * @param input
   *          the xml file.
   * @return the {@link MappingDescriptor} describe in the given xml input
   * @throws BundledException
   *           if an error occured
   * @see com.genia.toolbox.projects.csv.business.manager.MappingDescriptorManager#getMappingDescriptor(java.io.InputStream)
   */
  public MappingDescriptor getMappingDescriptor(InputStream input)
      throws BundledException
  {
    MappingDescriptor mappingDescriptor = (MappingDescriptor) getXmlMappingManager().getUnmarshaller().unmarshal(input);
    init(mappingDescriptor);
    return mappingDescriptor;
  }



  /**
   * getter for the xmlMappingManager property.
   * 
   * @return the xmlMappingManager
   */
  public XmlMappingManager getXmlMappingManager()
  {
    return xmlMappingManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * setter for the xmlMappingManager property.
   * 
   * @param xmlMappingManager
   *          the xmlMappingManager to set
   */
  public void setXmlMappingManager(XmlMappingManager xmlMappingManager)
  {
    this.xmlMappingManager = xmlMappingManager;
  }



  /**
   * compute the priority of a {@link FolderDescriptor} to allow sorting it.
   * 
   * @param folderDescriptor
   *          the {@link FolderDescriptor} to compute the priority for
   * @param priorities
   *          the list of priorities
   * @return the computed priority of the folder
   * @throws MappingException
   *           if there is a circular dependency
   */
  private int getPriority(FolderDescriptor folderDescriptor, Map<FolderDescriptor, Integer> priorities)
      throws MappingException
  {
    Integer priority = priorities.get(folderDescriptor);
    if (priority == null) {
      priorities.put(folderDescriptor, -1);
      priority = 0;
      for (String dependentFolderName : folderDescriptor.getDependantFolders()) {
        int newPriority = getPriority(folderDescriptor.getMappingDescriptor().getFolder(dependentFolderName), priorities) + 1;
        if (newPriority > priority) {
          priority = newPriority;
        }
      }
      priorities.put(folderDescriptor, priority);
    }
    if (priority == -1) {
      throw new MappingException("There is a circular dependency of folders with foler " + folderDescriptor.getFolderName(), KEY_FOLDER_CIRCULAR_DEPENDENCY, folderDescriptor.getFolderName());
    }
    return priority;
  }



  /**
   * init an {@link EntityDescriptor}.
   * 
   * @param entityDescriptor
   *          the {@link EntityDescriptor} to init
   * @throws BundledException
   *           if an error occured
   */
  private void init(EntityDescriptor entityDescriptor)
      throws BundledException
  {
    if (entityDescriptor.getName() == null) {
      throw new MappingException("An entity of foler " + entityDescriptor.getFolderDescriptor().getFolderName() + "has no name defined.", KEY_ENTITY_WITH_NO_NAME, entityDescriptor
          .getFolderDescriptor().getFolderName());
    }
    if (entityDescriptor.getClass() == null) {
      throw new MappingException("Entity " + entityDescriptor.getName() + " of foler " + entityDescriptor.getFolderDescriptor().getFolderName() + "has no class defined.", KEY_ENTITY_WITH_NO_CLASS,
          entityDescriptor.getName(), entityDescriptor.getFolderDescriptor().getFolderName());
    }
    for (Binder binder : entityDescriptor.getBinders()) {
      binder.visit(INITIALIZER_BINDER_VISITOR);
    }
    // TODO finish checking entity properties.
  }



  /**
   * init a {@link FieldDescriptor}.
   * 
   * @param field
   *          the {@link FieldDescriptor} to init
   * @throws BundledException
   *           if an error occured
   */
  private void init(FieldDescriptor field)
      throws BundledException
  {
    MutableFieldDescriptor mutableFieldDescriptor = null;
    if (field instanceof MutableFieldDescriptor) {
      mutableFieldDescriptor = (MutableFieldDescriptor) field;
    }

    if (TYPE_BY_CLASS.containsKey(field.getFieldClass())) {
      if (mutableFieldDescriptor == null && field.getFieldType() != TYPE_BY_CLASS.get(field.getFieldClass())) {
        throw new MappingException("field " + field.getName() + " of folder " + field.getFolderDescriptor() + " has imcompatible fieldType and class", KEY_FIELD_TYPE_IS_INCOMPATIBLE_WITH_CLASS, field
            .getFolderDescriptor().getFolderName(), field.getName());
      }
      else {
        mutableFieldDescriptor.setFieldType(TYPE_BY_CLASS.get(field.getFieldClass()));
      }
    }

    if (CLASS_BY_TYPE.containsKey(field.getFieldType())) {
      if (mutableFieldDescriptor == null && field.getFieldClass() != CLASS_BY_TYPE.get(field.getFieldType())) {
        throw new MappingException("field " + field.getName() + " of folder " + field.getFolderDescriptor() + " has imcompatible fieldType and class", KEY_FIELD_TYPE_IS_INCOMPATIBLE_WITH_CLASS, field
            .getFolderDescriptor().getFolderName(), field.getName());
      }
      else {
        mutableFieldDescriptor.setFieldClass(CLASS_BY_TYPE.get(field.getFieldType()));
      }
    }

    if (I18nDynamicValue.class.isAssignableFrom(field.getFieldClass()) && !(field.getFieldType() == FieldType.I18N || field.getFieldType() == FieldType.I18N_BIND)) {
      throw new MappingException("field " + field.getName() + " of folder " + field.getFolderDescriptor() + " has imcompatible fieldType and class", KEY_FIELD_TYPE_IS_INCOMPATIBLE_WITH_CLASS, field
          .getFolderDescriptor().getFolderName(), field.getName());
    }

    // TODO check field definition.
  }



  /**
   * init a {@link FolderDescriptor}.
   * 
   * @param folderDescriptor
   *          the {@link FolderDescriptor} to init
   * @throws BundledException
   *           if an error occured
   */
  private void init(FolderDescriptor folderDescriptor)
      throws BundledException
  {
    for (FieldDescriptor field : folderDescriptor.getFields()) {
      init(field);
    }

    final Map<EntityDescriptor, Integer> priorities = new HashMap<EntityDescriptor, Integer>();
    for (EntityDescriptor entityDescriptor : folderDescriptor.getEntities()) {
      initAndGetPriority(entityDescriptor, priorities);
    }
    sortList(folderDescriptor.getEntities(), priorities);
    // TODO check folder definition.
  }



  /**
   * init a {@link MappingDescriptor}.
   * 
   * @param mappingDescriptor
   *          the {@link MappingDescriptor} to init
   * @throws BundledException
   *           if an error occured
   */
  private void init(MappingDescriptor mappingDescriptor)
      throws BundledException
  {
    if (mappingDescriptor.getMainLocale() == null || !mappingDescriptor.getSupportedLocales().contains(mappingDescriptor.getMainLocale())) {
      throw new MappingException("main language not suppported", KEY_LANGUAGE_NOT_SUPPORTED);
    }

    final Map<FolderDescriptor, Integer> priorities = new HashMap<FolderDescriptor, Integer>();
    for (FolderDescriptor folderDescriptor : mappingDescriptor.getFolders()) {
      getPriority(folderDescriptor, priorities);
      init(folderDescriptor);
    }
    sortList(mappingDescriptor.getFolders(), priorities);
    // TODO check mapping definition.
  }



  /**
   * compute the priority of a {@link EntityDescriptor} to allow sorting it.
   * 
   * @param entityDescriptor
   *          the {@link EntityDescriptor} to compute the priority for
   * @param priorities
   *          the list of priorities
   * @return the computed priority of the entity
   * @throws BundledException
   *           if an error occured
   */
  private int initAndGetPriority(EntityDescriptor entityDescriptor, Map<EntityDescriptor, Integer> priorities)
      throws BundledException
  {
    Integer priority = priorities.get(entityDescriptor);
    if (priority == null) {
      init(entityDescriptor);
      priorities.put(entityDescriptor, -1);
      EntityPriorityVisitor visitor = new EntityPriorityVisitor(entityDescriptor, priorities);
      for (Binder binder : entityDescriptor.getBinders()) {
        binder.visit(visitor);
      }
      priority = visitor.getComputedPriority();
      priorities.put(entityDescriptor, priority);
    }
    if (priority == -1) {
      throw new MappingException("There is a circular dependency of entities with entity " + entityDescriptor.getName() + " of foler " + entityDescriptor.getFolderDescriptor().getFolderName(),
          KEY_ENTITY_CIRCULAR_DEPENDENCY, entityDescriptor.getName(), entityDescriptor.getFolderDescriptor().getFolderName());
    }
    return priority;
  }



  /**
   * sort a {@link List} of element given a map with their priorities.
   * 
   * @param <TYPE>
   *          the type of the elements to sort
   * @param list
   *          the list of elements
   * @param priorities
   *          the priorities of the elements
   */
  private <TYPE> void sortList(List<TYPE> list, final Map<TYPE, Integer> priorities)
  {
    Collections.sort(list, new Comparator<TYPE>()
    {
      public int compare(TYPE object1, TYPE object2)
      {
        return priorities.get(object1).compareTo(priorities.get(object2));
      }
    });
  }

}
